Skip to content

Unify DB initialization and app_read grants#386

Merged
jirhiker merged 3 commits into
stagingfrom
migration-permissions
Jan 16, 2026
Merged

Unify DB initialization and app_read grants#386
jirhiker merged 3 commits into
stagingfrom
migration-permissions

Conversation

@jirhiker

@jirhiker jirhiker commented Jan 15, 2026

Copy link
Copy Markdown
Member

Why

This PR addresses the following problem / context:

  • PUBLIC could previously inherit the app_read role, unintentionally giving every database user read access.
  • Transfer drop/rebuild and test initialization duplicated schema setup/grant logic, risking drift.

How

Implementation summary - the following was changed / added / removed:

  • Added db/initialization.py with helpers to recreate public, sync TSVector triggers, and grant app_read only to roles listed in the new APP_READ_MEMBERS env var.
  • Updated Alembic’s env script, transfer pipeline, and pytest DB fixture to use the shared helpers so every entry point reuses the same initialization + controlled grants.

Notes

Any special considerations, workarounds, or follow-up work to note?

  • Set APP_READ_MEMBERS (comma-separated role names) wherever migrations/tests run; if unset, no additional roles inherit app_read.

Copilot AI review requested due to automatic review settings January 15, 2026 17:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR consolidates database initialization logic by extracting common schema setup and search vector synchronization code into reusable utility functions in a new db/initialization.py module, and adds app_read role grants to the public schema.

Changes:

  • Created db/initialization.py with recreate_public_schema() and sync_search_vector_triggers() functions
  • Updated transfers/transfer.py and tests/conftest.py to use the new utility functions
  • Added GRANT app_read TO PUBLIC statement to both the Alembic migration and the initialization utilities

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
db/initialization.py New module containing shared database initialization utilities for schema recreation and search vector trigger synchronization
transfers/transfer.py Replaced inline SQL with calls to new utility functions and added search vector trigger synchronization step
tests/conftest.py Replaced inline SQL with calls to new utility functions
alembic/env.py Added GRANT app_read TO PUBLIC statement after role creation

Comment thread transfers/transfer.py
Comment thread db/initialization.py Outdated
Copilot AI review requested due to automatic review settings January 16, 2026 23:25
@jirhiker jirhiker merged commit 9e942ab into staging Jan 16, 2026
5 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 40 changed files in this pull request and generated 7 comments.

Comment thread transfers/util.py
f.write(data)

return pd.read_csv(io.BytesIO(data), dtype=dtype, *args, **kw)
return pd.read_csv(io.BytesIO(data), dtype=dtype)

Copilot AI Jan 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The read_csv function signature previously accepted *args, **kw to pass additional arguments to pd.read_csv, but the implementation now only passes dtype. This breaks backward compatibility for any callers that rely on passing other pandas read_csv parameters like parse_dates or keep_default_na. Since multiple files in this PR still call read_csv with parse_dates (e.g., chemistry_sampleinfo.py, major_chemistry.py), this change will cause runtime errors.

Suggested change
return pd.read_csv(io.BytesIO(data), dtype=dtype)
return pd.read_csv(io.BytesIO(data), dtype=dtype, *args, **kw)

Copilot uses AI. Check for mistakes.
Comment thread db/thing.py
Comment on lines 420 to 422
sorted_measuring_point_history = sorted(
self.measuring_points, key=lambda x: x.start_date, reverse=True
)

Copilot AI Jan 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removal of the empty check (if not self.measuring_points: return None) before accessing self.measuring_points[0] will cause an IndexError if a water well has no measuring points. The code assumes all water wells have at least one measuring point, but this may not be guaranteed during data migration or for incomplete records.

Copilot uses AI. Check for mistakes.
Comment thread db/thing.py
Comment on lines 436 to 438
sorted_measuring_point_history = sorted(
self.measuring_points, key=lambda x: x.start_date, reverse=True
)

Copilot AI Jan 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removal of the empty check (if not self.measuring_points: return None) before accessing self.measuring_points[0] will cause an IndexError if a water well has no measuring points. The code assumes all water wells have at least one measuring point, but this may not be guaranteed during data migration or for incomplete records.

Copilot uses AI. Check for mistakes.
"first",
row.OwnerKey,
email=str(row.Email).strip() if row.Email is not None else None,
email=row.Email.strip(),

Copilot AI Jan 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will raise an AttributeError if row.Email is None, since None has no .strip() method. The previous code checked if row.Email is not None before calling .strip(). Consider adding a null check or using row.Email.strip() if row.Email else None.

Copilot uses AI. Check for mistakes.
Comment thread schemas/location.py
@@ -129,8 +130,6 @@ class LocationGeoJSONResponse(BaseModel):
@model_validator(mode="before")
@classmethod
def populate_fields(cls, data: Any) -> Any:

Copilot AI Jan 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the if data is None: return None guard at the start of populate_fields can cause AttributeError when data is None and the method tries to access data.__table__. This guard was protecting against None values being passed to this validator.

Copilot uses AI. Check for mistakes.
Comment thread tests/test_thing.py
returned_ids = {item["id"] for item in data["items"]}
assert water_well_thing.id in returned_ids
assert spring_thing.id in returned_ids
assert data["total"] == 2

Copilot AI Jan 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test now asserts an exact count (== 2) instead of a minimum (>= 2). This makes the test more brittle, as it will fail if any other test in the suite creates additional Thing records that aren't properly cleaned up. Consider reverting to >= or ensuring test isolation with proper cleanup.

Suggested change
assert data["total"] == 2
assert data["total"] >= 2

Copilot uses AI. Check for mistakes.
Comment thread tests/test_location.py
assert item["created_at"] == location.created_at.astimezone(timezone.utc).strftime(
DT_FMT
)
assert data["total"] == 1

Copilot AI Jan 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test now asserts an exact count (== 1) instead of a minimum (>= 1). This makes the test more brittle, as it will fail if any other test in the suite creates additional Location records that aren't properly cleaned up. Consider reverting to >= or ensuring test isolation with proper cleanup.

Copilot uses AI. Check for mistakes.
@TylerAdamMartinez TylerAdamMartinez deleted the migration-permissions branch February 26, 2026 18:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants